// Copyright (C) 2013-2024 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

#ifndef CALLOUT_HANDLE_H
#define CALLOUT_HANDLE_H

#include <exceptions/exceptions.h>
#include <hooks/library_handle.h>
#include <hooks/parking_lots.h>
#include <util/dhcp_space.h>

#include <boost/any.hpp>
#include <boost/shared_ptr.hpp>

#include <map>
#include <string>
#include <vector>

namespace isc {
namespace hooks {

class ServerHooks;

/// @brief No such argument
///
/// Thrown if an attempt is made access an argument that does not exist.

class NoSuchArgument : public Exception {
public:
    NoSuchArgument(const char* file, size_t line, const char* what) :
        isc::Exception(file, line, what) {}
};

/// @brief No such callout context item
///
/// Thrown if an attempt is made to get an item of data from this callout's
/// context and either the context or an item in the context with that name
/// does not exist.

class NoSuchCalloutContext : public Exception {
public:
    NoSuchCalloutContext(const char* file, size_t line, const char* what) :
        isc::Exception(file, line, what) {}
};

// Forward declaration of the library handle and related collection classes.

class CalloutManager;
class LibraryManagerCollection;

/// @brief Per-packet callout handle
///
/// An object of this class is associated with every packet (or request)
/// processed by the server.  It forms the principle means of passing data
/// between the server and the user-library callouts.
///
/// The class allows access to the following information:
///
/// - Arguments.  When the callouts associated with a hook are called, they
///   are passed information by the server (and can return information to it)
///   through name/value pairs.  Each of these pairs is an argument and the
///   information is accessed through the {get,set}Argument() methods.
///
/// - Per-packet context.  Each packet has a context associated with it, this
///   context being  on a per-library basis.  In other words, As a packet passes
///   through the callouts associated with a given library, the callouts can
///   associate and retrieve information with the packet.  The per-library
///   nature of the context means that the callouts within a given library can
///   pass packet-specific information between one another, but they cannot pass
///   information to callous within another library.  Typically such context
///   is created in the "context_create" callout and destroyed in the
///   "context_destroy" callout.  The information is accessed through the
///   {get,set}Context() methods.

class CalloutHandle {
public:

    /// @brief Specifies allowed next steps
    ///
    /// Those values are used to designate the next step in packet processing.
    /// They are set by hook callouts and read by the Kea server. See
    /// @ref setStatus for detailed description of each value.
    enum CalloutNextStep {
        NEXT_STEP_CONTINUE = 0, ///< continue normally
        NEXT_STEP_SKIP = 1,     ///< skip the next processing step
        NEXT_STEP_DROP = 2,     ///< drop the packet
        NEXT_STEP_PARK = 3      ///< park the packet
    };


    /// Typedef to allow abbreviation of iterator specification in methods.
    /// The std::string is the argument name and the "boost::any" is the
    /// corresponding value associated with it.
    typedef std::map<std::string, boost::any> ElementCollection;

    /// Typedef to allow abbreviations in specifications when accessing
    /// context.  The ElementCollection is the name/value collection for
    /// a particular context.  The "int" corresponds to the index of an
    /// associated library - there is a 1:1 correspondence between libraries
    /// and a name.value collection.
    ///
    /// The collection of contexts is stored in a map, as not every library
    /// will require creation of a context associated with each packet.  In
    /// addition, the structure is more flexible in that the size does not
    /// need to be set when the CalloutHandle is constructed.
    typedef std::map<int, ElementCollection> ContextCollection;

    /// @brief Constructor
    ///
    /// Creates the object and calls the callouts on the "context_create"
    /// hook.
    ///
    /// Of the two arguments passed, only the pointer to the callout manager is
    /// actively used.  The second argument, the pointer to the library manager
    /// collection, is used for lifetime control: after use, the callout handle
    /// may contain pointers to memory allocated by the loaded libraries.  The
    /// used of a shared pointer to the collection of library managers means
    /// that the libraries that could have allocated memory in a callout handle
    /// will not be unloaded until all such handles have been destroyed.  This
    /// issue is discussed in more detail in the documentation for
    /// isc::hooks::LibraryManager.
    ///
    /// @param manager Pointer to the callout manager object.
    /// @param lmcoll Pointer to the library manager collection.  This has a
    ///        null default for testing purposes.
    CalloutHandle(const boost::shared_ptr<CalloutManager>& manager,
                  const boost::shared_ptr<LibraryManagerCollection>& lmcoll =
                        boost::shared_ptr<LibraryManagerCollection>());

    /// @brief Destructor
    ///
    /// Calls the context_destroy callback to release any per-packet context.
    /// It also clears stored data to avoid problems during member destruction.
    ~CalloutHandle();

    /// @brief Set argument
    ///
    /// Sets the value of an argument.  The argument is created if it does not
    /// already exist.
    ///
    /// @param name Name of the argument.
    /// @param value Value to set.  That can be of any data type.
    template <typename T>
    void setArgument(const std::string& name, T value) {
        arguments_[name] = value;
    }

    /// @brief Get argument
    ///
    /// Gets the value of an argument.
    ///
    /// @param name Name of the element in the argument list to get.
    /// @param value [out] Value to set.  The type of "value" is important:
    ///        it must match the type of the value set.
    ///
    /// @throw NoSuchArgument No argument with the given name is present.
    /// @throw boost::bad_any_cast An argument with the given name is present,
    ///        but the data type of the value is not the same as the type of
    ///        the variable provided to receive the value.
    template <typename T>
    void getArgument(const std::string& name, T& value) const {
        ElementCollection::const_iterator element_ptr = arguments_.find(name);
        if (element_ptr == arguments_.end()) {
            isc_throw(NoSuchArgument, "unable to find argument with name " <<
                      name);
        }

        value = boost::any_cast<T>(element_ptr->second);
    }

    /// @brief Get argument names
    ///
    /// Returns a vector holding the names of arguments in the argument
    /// vector.
    ///
    /// @return Vector of strings reflecting argument names.
    std::vector<std::string> getArgumentNames() const;

    /// @brief Delete argument
    ///
    /// Deletes an argument of the given name.  If an argument of that name
    /// does not exist, the method is a no-op.
    ///
    /// N.B. If the element is a raw pointer, the pointed-to data is NOT deleted
    /// by this method.
    ///
    /// @param name Name of the element in the argument list to set.
    void deleteArgument(const std::string& name) {
        static_cast<void>(arguments_.erase(name));
    }

    /// @brief Delete all arguments
    ///
    /// Deletes all arguments associated with this context.
    ///
    /// N.B. If any elements are raw pointers, the pointed-to data is NOT
    /// deleted by this method.
    void deleteAllArguments() {
        arguments_.clear();
    }

    /// @brief Sets the next processing step.
    ///
    /// This method is used by the callouts to determine the next step
    /// in processing. This method replaces former setSkip() method
    /// that allowed only two values.
    ///
    /// Currently there are three possible value allowed:
    /// NEXT_STEP_CONTINUE - tells the server to continue processing as usual
    ///                      (equivalent of previous setSkip(false) )
    ///
    /// NEXT_STEP_SKIP - tells the server to skip the processing. Exact meaning
    ///                  is hook specific. See hook documentation for details.
    ///                  (equivalent of previous setSkip(true))
    ///
    /// NEXT_STEP_DROP - tells the server to unconditionally drop the packet
    ///                  and do not process it further.
    ///
    /// NEXT_STEP_PARK - tells the server to "park" the packet. The packet will
    ///                  wait in the queue for being unparked, e.g. as a result
    ///                  of completion of the asynchronous performed by the
    ///                  hooks library operation.
    ///
    /// This variable is interrogated by the server to see if the remaining
    /// callouts associated with the current hook should be bypassed.
    ///
    /// @param next New value of the next step status.
    void setStatus(const CalloutNextStep next) {
        next_step_ = next;
    }

    /// @brief Returns the next processing step.
    ///
    /// Gets the current value of the next step. See @ref setStatus for detailed
    /// definition.
    ///
    /// @return Current value of the skip flag.
    CalloutNextStep getStatus() const {
        return (next_step_);
    }

    /// @brief Set context
    ///
    /// Sets an element in the context associated with the current library.  If
    /// an element of the name is already present, it is replaced.
    ///
    /// @param name Name of the element in the context to set.
    /// @param value Value to set.
    template <typename T>
    void setContext(const std::string& name, T value) {
        getContextForLibrary()[name] = value;
    }

    /// @brief Get context
    ///
    /// Gets an element from the context associated with the current library.
    ///
    /// @param name Name of the element in the context to get.
    /// @param value [out] Value to set.  The type of "value" is important:
    ///        it must match the type of the value set.
    ///
    /// @throw NoSuchCalloutContext Thrown if no context element with the name
    ///        "name" is present.
    /// @throw boost::bad_any_cast Thrown if the context element is present
    ///        but the type of the data is not the same as the type of the
    ///        variable provided to receive its value.
    template <typename T>
    void getContext(const std::string& name, T& value) const {
        const ElementCollection& lib_context = getContextForLibrary();

        ElementCollection::const_iterator element_ptr = lib_context.find(name);
        if (element_ptr == lib_context.end()) {
            isc_throw(NoSuchCalloutContext, "unable to find callout context "
                      "item " << name << " in the context associated with "
                      "current library");
        }

        value = boost::any_cast<T>(element_ptr->second);
    }

    /// @brief Fetch an optional named element from the current library
    /// context.
    ///
    /// Gets an element from the context associated with the current library.
    /// If either context or the named element do not exist, the function
    /// returns false.
    ///
    /// @param name Name of the element in the context to get.
    /// @param value [out] Value to set.  The type of "value" is important:
    ///        it must match the type of the value set.
    ///
    /// @throw boost::bad_any_cast Thrown if the context element is present
    ///        but the type of the data is not the same as the type of the
    ///        variable provided to receive its value.
    /// @return true if the named element exists, false otherwise.
    template <typename T>
    bool getOptionalContext(const std::string& name, T& value) const {
        const auto context_iter = context_collection_.find(current_library_);
        if (context_iter == context_collection_.end()) {
            // No context, punt.
            return (false);
        }

        auto lib_context = context_iter->second;
        ElementCollection::const_iterator element_ptr = lib_context.find(name);
        if (element_ptr == lib_context.end()) {
            return (false);
        }

        value = boost::any_cast<T>(element_ptr->second);
        return (true);
    }

    /// @brief Get context names
    ///
    /// Returns a vector holding the names of items in the context associated
    /// with the current library.
    ///
    /// @return Vector of strings reflecting the names of items in the callout
    ///         context associated with the current library.
    std::vector<std::string> getContextNames() const;

    /// @brief Delete context element
    ///
    /// Deletes an item of the given name from the context associated with the
    /// current library.  If an item  of that name does not exist, the method is
    /// a no-op.
    ///
    /// N.B. If the element is a raw pointer, the pointed-to data is NOT deleted
    /// by this.
    ///
    /// @param name Name of the context item to delete.
    void deleteContext(const std::string& name) {
        static_cast<void>(getContextForLibrary().erase(name));
    }

    /// @brief Delete all context items
    ///
    /// Deletes all items from the context associated with the current library.
    ///
    /// N.B. If any elements are raw pointers, the pointed-to data is NOT
    /// deleted by this.
    void deleteAllContext() {
        getContextForLibrary().clear();
    }

    /// @brief Get hook name
    ///
    /// Get the name of the hook to which the current callout is attached.
    /// This can be the null string if the CalloutHandle is being accessed
    /// outside of the CalloutManager's "callCallouts" method.
    ///
    /// @return Name of the current hook or the empty string if none.
    std::string getHookName() const;

    /// @brief Returns pointer to the parking lot handle for this hook point.
    ///
    /// @return pointer to the parking lot handle
    ParkingLotHandlePtr getParkingLotHandlePtr() const;

    /// @brief Get current library index
    ///
    /// @return The current library index
    int getCurrentLibrary() const {
        return (current_library_);
    }

    /// @brief Set current library index
    ///
    /// @param library_index The library index
    void setCurrentLibrary(int library_index) {
        current_library_ = library_index;
    }

    /// @brief Get current hook index
    ///
    /// @return The current hook index
    int getCurrentHook() const {
        return (current_hook_);
    }

    /// @brief Set current hook index
    ///
    /// @param hook_index The hook index
    void setCurrentHook(int hook_index) {
        current_hook_ = hook_index;
    }

private:

    /// @brief Check index
    ///
    /// Gets the current library index, throwing an exception if it is not set
    /// or is invalid for the current library collection.
    ///
    /// @return Current library index, valid for this library collection.
    ///
    /// @throw InvalidIndex current library index is not valid for the library
    ///        handle collection.
    int getLibraryIndex() const;

    /// @brief Return reference to context for current library
    ///
    /// Called by all context-setting functions, this returns a reference to
    /// the callout context for the current library, creating a context if it
    /// does not exist.
    ///
    /// @return Reference to the collection of name/value pairs associated
    ///         with the current library.
    ///
    /// @throw InvalidIndex current library index is not valid for the library
    ///        handle collection.
    ElementCollection& getContextForLibrary();

    /// @brief Return reference to context for current library (const version)
    ///
    /// Called by all context-accessing functions, this a reference to the
    /// callout context for the current library.  An exception is thrown if
    /// it does not exist.
    ///
    /// @return Reference to the collection of name/value pairs associated
    ///         with the current library.
    ///
    /// @throw NoSuchCalloutContext Thrown if there is no ElementCollection
    ///        associated with the current library.
    const ElementCollection& getContextForLibrary() const;

    // Member variables

    /// Pointer to the collection of libraries for which this handle has been
    /// created.
    boost::shared_ptr<LibraryManagerCollection> lm_collection_;

    /// Collection of arguments passed to the callouts
    ElementCollection arguments_;

    /// Context collection - there is one entry per library context.
    ContextCollection context_collection_;

    /// Callout manager.
    boost::shared_ptr<CalloutManager> manager_;

    /// Reference to the singleton ServerHooks object.  See the
    /// @ref hooksmgMaintenanceGuide for information as to why the class holds
    /// a reference instead of accessing the singleton within the code.
    ServerHooks& server_hooks_;

    /// @brief Current library.
    ///
    /// When a call is made to @ref CalloutManager::callCallouts, this holds
    /// the index of the current library.  It is set to an invalid value (-1)
    /// otherwise.
    int current_library_;

    /// @brief Current hook.
    ///
    /// When a call is made to @ref CalloutManager::callCallouts, this holds
    /// the index of the current hook.  It is set to an invalid value  (-1)
    /// otherwise.
    int current_hook_;

    /// Next processing step, indicating what the server should do next.
    CalloutNextStep next_step_;
};

/// A shared pointer to a CalloutHandle object.
typedef boost::shared_ptr<CalloutHandle> CalloutHandlePtr;

/// @brief Wrapper class around callout handle which automatically
/// resets handle's state.
///
/// The Kea servers often require to associate processed packets with
/// @c CalloutHandle instances. This is to facilitate the case when the
/// hooks library passes information between the callouts using the
/// 'context' stored in the callout handle. The callouts invoked throughout
/// the packet lifetime have access to the context information for the
/// given packet.
///
/// The association between the packets and the callout handles is
/// achieved by giving the ownership of the @c CalloutHandle objects to
/// the @c Pkt objects. When the @c Pkt object goes out of scope, it should
/// also release the pointer to the owned @c CalloutHandle object.
/// However, this causes a risk of circular dependency between the shared
/// pointer to the @c Pkt object and the shared pointer to the
/// @c CalloutHandle it owns, because the pointer to the packet is often
/// set as an argument of the callout handle prior to invoking a callout.
///
/// In order to break the circular dependency, the arguments of the
/// callout handle must be deleted as soon as they are not needed
/// anymore. This class is a wrapper around the callout handle object,
/// which resets its state during construction and destruction. All
/// Kea hook points must use this class within the scope where the
/// @c HooksManager::callCallouts is invoked to reset the state of the
/// callout handle. The state is reset when this object goes out of
/// scope.
///
/// Currently, the following operations are performed during the reset:
/// - all arguments of the callout handle are deleted,
/// - the next step status is set to @c CalloutHandle::NEXT_STEP CONTINUE
///
/// This class must never be modified to also delete the context
/// information from the callout handle. The context is intended
/// to be used to share stateful data across callouts and hook points
/// and its contents must exist for the duration of the packet lifecycle.
/// Otherwise, we could simply re-create the callout handle for
/// each hook point and we wouldn't need this RAII class.
class ScopedCalloutHandleState {
public:

    /// @brief Constructor.
    ///
    /// Resets state of the callout handle.
    ///
    /// @param callout_handle reference to the pointer to the callout
    /// handle which state should be reset.
    /// @throw isc::BadValue if the callout handle is null.
    explicit ScopedCalloutHandleState(const CalloutHandlePtr& callout_handle);

    /// @brief Destructor.
    ///
    /// Resets state of the callout handle.
    ~ScopedCalloutHandleState();

    /// @brief Continuation callback.
    std::function<void()> on_completion_;

private:

    /// @brief Resets the callout handle state.
    ///
    /// It is used internally by the constructor and destructor.
    void resetState();

    /// @brief Holds pointer to the wrapped callout handle.
    CalloutHandlePtr callout_handle_;
};

} // namespace hooks
} // namespace isc


#endif // CALLOUT_HANDLE_H
